fix(native): resolve monorepo workspace package imports in the native engine#2061
Conversation
… engine resolve_import_path_inner had no workspace-awareness at all: a bare monorepo-package specifier (e.g. import "@myorg/lib") fell straight through to the raw-specifier fallback under the native engine, unlike the WASM/JS engine's resolveViaWorkspace(), which resolves it to the package's real entry file and grants the resulting call edges a 0.95 confidence floor. - Port resolveViaWorkspace()/parseBareSpecifier() into crates/codegraph-core/src/domain/graph/resolve.rs (resolve_via_workspace, parse_bare_specifier), threaded through resolve_import_path/ resolve_imports_batch and both the per-call FFI path (lib.rs) and the full Rust build orchestrator (pipeline.rs, import_edges.rs). - Add a process-lifetime workspace-resolved-paths cache in resolve.rs, mirroring _workspaceResolvedPaths in resolve.ts, so compute_confidence grants the same 0.95 confidence floor natively. - Thread the already-detected workspace map (from detectWorkspaces() in infrastructure/config.ts — no native equivalent, matching the existing config.rs JSON-handoff pattern) across the FFI boundary: new NativeWorkspacePackage/WorkspacePackage type, new optional parameters on resolveImport/resolveImports/buildGraph, and a new getWorkspacesForNative() helper in resolve.ts. - Add a monorepo-workspace fixture and wire it into the native/WASM build-parity hard gate, plus a dedicated regression test asserting the resolved edge, its target file, and the 0.95 confidence floor for both engines. Native still has no package.json "exports" field resolver (a separate, broader gap tracked in #2060) — resolve_via_workspace falls back to main/source/index-file resolution for that case, same as a repo without an exports field would get under either engine.
Codegraph Impact Analysis14 functions changed → 32 callers affected across 8 files
|
Greptile SummaryThis PR ports the JS/WASM
Confidence Score: 4/5Safe to merge; the workspace resolution logic is correct, all pipeline paths receive the workspace map, and 21 targeted Rust unit tests plus a parity gate guard the new behavior. The core algorithm and cache-reset contract are sound and well-covered. The two findings are both non-blocking: the integration test's confidence assertion is satisfied by the 1.0 path rather than the intended 0.95 floor (a coverage gap, not a behavioral regression), and the silent skip on a poisoned mutex in reset is a theoretical concern that requires a panic-while-locked scenario to manifest. resolve.rs — the process-lifetime workspace cache and its reset contract are the most critical logic to re-read; the test file issue-1927-native-workspace-resolution.test.ts should have its confidence assertion revisited if the 0.95 floor specifically needs integration-level coverage. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant JS as resolve.ts (JS)
participant FFI as lib.rs (napi FFI)
participant RES as resolve.rs (Rust)
participant CACHE as workspace_resolved_cache (global Mutex)
participant CONF as compute_confidence
Note over JS,CACHE: Per-call FFI path (JS-driven build)
JS->>JS: detectWorkspaces() → workspace map
JS->>JS: getWorkspacesForNative(rootDir)
JS->>FFI: resolveImports(inputs, rootDir, aliases, knownFiles, workspaces)
FFI->>CACHE: reset_workspace_resolved_paths()
FFI->>RES: resolve_imports_batch(..., Some(workspaces))
loop par_iter over imports
RES->>RES: resolve_import_path_inner()
RES->>RES: resolve_non_relative_import() → resolve_via_workspace()
RES->>CACHE: mark_workspace_resolved(rel_path)
end
RES-->>FFI: Vec ResolvedImport
FFI-->>JS: resolved paths
JS->>FFI: computeConfidence(callerFile, targetFile, importedFrom)
FFI->>CONF: compute_confidence()
CONF->>CACHE: is_workspace_resolved(imp)
CACHE-->>CONF: true/false
CONF-->>FFI: 0.95 (workspace floor) or proximity score
FFI-->>JS: confidence
Note over JS,CACHE: Rust orchestrator path (buildGraph)
JS->>FFI: buildGraph(rootDir, configJson, aliasesJson, optsJson, workspacesJson)
FFI->>FFI: pipeline_setup() → reset_workspace_resolved_paths()
FFI->>RES: resolve_imports_batch(..., Some(workspaces))
RES->>CACHE: mark_workspace_resolved(...)
FFI->>CONF: compute_confidence() reads cache
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant JS as resolve.ts (JS)
participant FFI as lib.rs (napi FFI)
participant RES as resolve.rs (Rust)
participant CACHE as workspace_resolved_cache (global Mutex)
participant CONF as compute_confidence
Note over JS,CACHE: Per-call FFI path (JS-driven build)
JS->>JS: detectWorkspaces() → workspace map
JS->>JS: getWorkspacesForNative(rootDir)
JS->>FFI: resolveImports(inputs, rootDir, aliases, knownFiles, workspaces)
FFI->>CACHE: reset_workspace_resolved_paths()
FFI->>RES: resolve_imports_batch(..., Some(workspaces))
loop par_iter over imports
RES->>RES: resolve_import_path_inner()
RES->>RES: resolve_non_relative_import() → resolve_via_workspace()
RES->>CACHE: mark_workspace_resolved(rel_path)
end
RES-->>FFI: Vec ResolvedImport
FFI-->>JS: resolved paths
JS->>FFI: computeConfidence(callerFile, targetFile, importedFrom)
FFI->>CONF: compute_confidence()
CONF->>CACHE: is_workspace_resolved(imp)
CACHE-->>CONF: true/false
CONF-->>FFI: 0.95 (workspace floor) or proximity score
FFI-->>JS: confidence
Note over JS,CACHE: Rust orchestrator path (buildGraph)
JS->>FFI: buildGraph(rootDir, configJson, aliasesJson, optsJson, workspacesJson)
FFI->>FFI: pipeline_setup() → reset_workspace_resolved_paths()
FFI->>RES: resolve_imports_batch(..., Some(workspaces))
RES->>CACHE: mark_workspace_resolved(...)
FFI->>CONF: compute_confidence() reads cache
|
| pub fn reset_workspace_resolved_paths() { | ||
| if let Ok(mut set) = workspace_resolved_cache().lock() { | ||
| set.clear(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Silent no-op on poisoned mutex can leave stale workspace paths across builds
If a thread panics while holding workspace_resolved_cache()'s lock, the mutex becomes poisoned and if let Ok(...) silently skips the clear(). Any subsequent build will then see workspace-resolved paths left over from the panicking build, causing compute_confidence to incorrectly award the 0.95 floor to paths that are no longer workspace-resolved. Using unwrap_or_else(|p| p.into_inner()) recovers the inner data from a poisoned guard rather than abandoning the clear entirely.
| pub fn reset_workspace_resolved_paths() { | |
| if let Ok(mut set) = workspace_resolved_cache().lock() { | |
| set.clear(); | |
| } | |
| } | |
| pub fn reset_workspace_resolved_paths() { | |
| let mut set = workspace_resolved_cache() | |
| .lock() | |
| .unwrap_or_else(|poisoned| poisoned.into_inner()); | |
| set.clear(); | |
| } |
Summary
resolve_import_path_innerin the native (Rust) engine had no workspace-awareness at all: a bare monorepo-package specifier (e.g.import "@myorg/lib") fell straight through to the raw-specifier fallback under native, unlike the WASM/JS engine'sresolveViaWorkspace(), which resolves it to the package's real entry file — and grants the resulting cross-package call edges a 0.95 confidence floor via_workspaceResolvedPaths.Changes
crates/codegraph-core/src/domain/graph/resolve.rs: portsresolveViaWorkspace()/parseBareSpecifier()(resolve_via_workspace,parse_bare_specifier), threaded throughresolve_import_path/resolve_imports_batch. Adds a process-lifetime workspace-resolved-paths cache mirroring_workspaceResolvedPathsinresolve.ts, read bycompute_confidence()to grant the same 0.95 floor natively — with no changes needed tocompute_confidence's call sites.crates/codegraph-core/src/lib.rs/db/connection.rs/domain/graph/builder/pipeline.rs/domain/graph/builder/stages/import_edges.rs: threads the already-detected workspace map through both call paths — the per-call FFI functions (resolve_import/resolve_imports) and the full Rust build orchestrator (NativeDatabase::build_graph→run_pipeline→resolve_pipeline_imports/barrel re-parse/ImportEdgeContext).crates/codegraph-core/src/types.rs: newWorkspacePackagenapi/serde type ({packageName, dir, entry}), reused for both the FFI array parameter and theworkspaces_jsonblob deserialized by the orchestrator (needs its own#[serde(rename_all = "camelCase")], same reasonBuildPathAliasesexists alongsidePathAliases).src/domain/graph/resolve.ts: newgetWorkspacesForNative()helper converts the JS-side workspace map (populated bydetectWorkspaces()— no native equivalent, matching the established config.rs JSON-handoff pattern) into the FFI shape; wired intoresolveImportPath()/resolveImportsBatch().src/domain/graph/builder/stages/native-orchestrator.ts/src/types.ts: newworkspacesJsonargument onbuildGraph().tests/fixtures/monorepo-workspace/fixture wired into the existing native/WASMbuild-parity.test.tshard gate, plus a dedicatedtests/integration/issue-1927-native-workspace-resolution.test.tsasserting the resolved import edge, its target file, and the 0.95 confidence floor for both engines. 21 new Rust unit tests coverparse_bare_specifier,resolve_via_workspace,resolve_non_relative_import,resolve_import_path,compute_confidence, and the cache reset contract.Scope note
The native engine still has no
package.jsonexports-field resolver at all (resolveViaExports()has no Rust counterpart — a separate, pre-existing, broader gap that also affects plainnode_modulesbare specifiers, not just workspace packages).resolve_via_workspacetherefore skips that lookup and falls straight tomain/source/index-file resolution, same as a repo without anexportsfield would get under either engine. Filed as #2060 — out of scope for this fix.Test plan
cargo test(codegraph-core): 640 passed, 0 failed (21 new)cargo clippy: no new warnings on touched filesnpx tsc --noEmit: cleannpm run lint(Biome): cleannpx vitest run tests/unit/resolve.test.ts: 67 passed (pre-existing JS-side coverage, unaffected)npx vitest run tests/integration/issue-1927-native-workspace-resolution.test.ts: 6 passed — bothwasmandnativeengines resolve the workspace import to the same target file and grant the same 0.95 confidencenpx vitest run tests/integration/build-parity.test.ts -t monorepo-workspace: 4 passed — native and WASM produce byte-identical nodes/edges/roles/ast_nodes for the new fixturenapi build --platform --release, codesigned) and verified directly against the exact repro shape from the issuenpm test: run twice against this worktree's local WASM grammar cache (a pre-existing, only-partially-built cache unrelated to this change); both runs produced the identical 213/4103 failure set, 100% attributable to missing grammars for languages this fix does not touch (objc, julia, swift, elixir, dynamic-groovy, dynamic-scala precision/recall thresholds, plus the correspondingtests/parsers/*.test.tsandtests/engines/parity.test.tsfiles for those same languages) — confirmed viaTHRESHOLDS[lang]inresolution-benchmark.test.tsthat every other 0%-coverage language already has an intentionally low/zero threshold unrelated to grammar availability. No test touching import/workspace resolution, confidence scoring, or the native build orchestrator failed.Closes #1927